/*
 * TaskProvider.java
 *
 * Copyright (C) 2012 Marten Gajda <marten@dmfs.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published
 * by the Free Software Foundation; either version 2 of the License,
 * or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 */

package ms.jung.andorid.caldavtodo;

import ms.jung.andorid.caldavtodo.TaskDatabaseHelper.Tables;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;

/**
 * The provider for tasks.
 * 
 * TODO: add support for efficient batch operations
 * 
 * TODO: add support for transactions
 * 
 * TODO: add support for recurring tasks
 * 
 * TODO: add support for reminders
 * 
 * TODO: add support for attendees
 * 
 * @author Marten Gajda <marten@dmfs.org>
 * 
 */
public final class TaskProvider extends ContentProvider {

    private static final int LISTS = 1;
    private static final int LIST_ID = 2;
    private static final int TASKS = 3;
    private static final int TASK_ID = 4;
    private static final int CATEGORIES = 5;
    private static final int CATEGORY_ID = 6;

    private static final UriMatcher uriMatcher;

    private SQLiteDatabase mTaskDb;

    protected boolean getIsCallerSyncAdapter(Uri uri) {
	String param = uri.getQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER);
	return param != null && !"false".equals(param);
    }

    protected String getAccountName(Uri uri) {
	return uri.getQueryParameter(TaskContract.ACCOUNT_NAME);
    }

    protected String getAccountType(Uri uri) {
	return uri.getQueryParameter(TaskContract.ACCOUNT_TYPE);
    }

    protected String getId(Uri uri) {
	return uri.getPathSegments().get(1);
    }

    protected StringBuilder selectAccount(Uri uri) {
	StringBuilder sb = new StringBuilder(256);
	return selectAccount(sb, uri);
    }

    protected StringBuilder selectAccount(StringBuilder sb, Uri uri) {
	String accountName = getAccountName(uri);
	String accountType = getAccountType(uri);

	if (accountName != null || accountType != null) {

	    if (accountName != null) {
		if (sb.length() > 0) {
		    sb.append(" AND ");
		}

		sb.append(TaskContract.TaskLists.ACCOUNT_NAME);
		sb.append("=");
		DatabaseUtils.appendEscapedSQLString(sb, accountName);
	    }
	    if (accountType != null) {

		if (sb.length() > 0) {
		    sb.append(" AND ");
		}

		sb.append(TaskContract.TaskLists.ACCOUNT_TYPE);
		sb.append("=");
		DatabaseUtils.appendEscapedSQLString(sb, accountType);
	    }
	}
	return sb;
    }

    protected StringBuilder selectId(Uri uri) {
	StringBuilder sb = new StringBuilder(128);
	return selectId(sb, uri);
    }

    protected StringBuilder selectId(StringBuilder sb, Uri uri) {
	if (sb.length() > 0) {
	    sb.append(" AND ");
	}
	sb.append(TaskContract.TaskLists._ID);
	sb.append("=");
	sb.append(getId(uri));
	return sb;
    }

    protected String updateSelection(StringBuilder sb, String selection) {
	if (selection != null) {
	    sb.append("AND ( ").append(selection).append(" ) ");
	}
	return sb.toString();
    }

    @Override
    public boolean onCreate() {
	Context context = getContext();
	TaskDatabaseHelper dbHelper = new TaskDatabaseHelper(context);
	mTaskDb = dbHelper.getWritableDatabase();
	return mTaskDb != null;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {

	SQLiteQueryBuilder sqlBuilder = new SQLiteQueryBuilder();
	boolean isSyncAdapter = getIsCallerSyncAdapter(uri);

	// add account to selection if any
	selection = updateSelection(selectAccount(uri), selection);

	switch (uriMatcher.match(uri)) {
	case LISTS:
	    sqlBuilder.setTables(Tables.LISTS);
	    if (sortOrder == null || sortOrder == "")
		sortOrder = TaskContract.TaskLists.DEFAULT_SORT_ORDER;

	    break;

	case LIST_ID:
	    sqlBuilder.setTables(Tables.LISTS);
	    sqlBuilder.appendWhere(TaskContract.TaskLists._ID + " = " + getId(uri));
	    if (sortOrder == null || sortOrder == "")
		sortOrder = TaskContract.TaskLists.DEFAULT_SORT_ORDER;
	    break;

	case TASKS:
	    sqlBuilder.setTables(Tables.TASKS);
	    if (! isSyncAdapter) {
		// do not return deleted rows if caller is not a sync adapter
		sqlBuilder.appendWhere("_DELETED=0");
	    }
	    if (sortOrder == null || sortOrder == "")
		sortOrder = TaskContract.TaskLists.DEFAULT_SORT_ORDER;
	    break;

	case TASK_ID:
	    sqlBuilder.setTables(Tables.TASKS);
	    sqlBuilder.appendWhere(TaskContract.Tasks._ID + " = " + getId(uri));
	    if (! isSyncAdapter) {
		// do not return deleted rows if caller is not a sync adapter
		sqlBuilder.appendWhere("_DELETED=0");
	    }
	    if (sortOrder == null || sortOrder == "")
		sortOrder = TaskContract.Tasks.DEFAULT_SORT_ORDER;
	    break;

	case CATEGORIES:
	    sqlBuilder.setTables(Tables.CATEGORIES);
	    if (sortOrder == null || sortOrder == "")
		sortOrder = TaskContract.TaskLists.DEFAULT_SORT_ORDER;
	    break;

	case CATEGORY_ID:
	    sqlBuilder.setTables(Tables.CATEGORIES);
	    sqlBuilder.appendWhere(TaskContract.Categories._ID + " = " + getId(uri));
	    if (sortOrder == null || sortOrder == "")
		sortOrder = TaskContract.Categories.DEFAULT_SORT_ORDER;
	    break;

	}

	Cursor c = sqlBuilder.query(mTaskDb, projection, selection, selectionArgs, null, null, sortOrder);

	if (c != null) {
	    c.setNotificationUri(getContext().getContentResolver(), uri);
	}
	return c;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {

	int count = 0;

	boolean isSyncAdapter = getIsCallerSyncAdapter(uri);
	String accountName = getAccountName(uri);
	String accountType = getAccountType(uri);

	switch (uriMatcher.match(uri)) {
	/*
	 * Deleting task lists is only allowed to sync adapters. They must provide ACCOUNT_NAME and ACCOUNT_TYPE.
	 */
	case LIST_ID:
	    // add _id to selection and fall through
	    selection = updateSelection(selectId(uri), selection);
	case LISTS:
	    if (isSyncAdapter) {

		if (TextUtils.isEmpty(accountType) || TextUtils.isEmpty(accountName)) {
		    throw new IllegalArgumentException("Sync adapters must specify an account and account type: " + uri);
		}

		selection = updateSelection(selectAccount(uri), selection);

		count = mTaskDb.delete(Tables.LISTS, selection, selectionArgs);
	    } else {
		throw new UnsupportedOperationException("Caller must be a sync adapter to delete task lists");
	    }
	    break;

	/*
	 * Task won't be removed, just marked as deleted if the caller isn't a sync adapter. Sync adapters can remove
	 * tasks immediately.
	 */
	case TASK_ID:
	    // add id to selection and fall through
	    selection = updateSelection(selectId(uri), selection);

	case TASKS:
	    selection = updateSelection(selectAccount(uri), selection);

	    if (isSyncAdapter) {

		if (TextUtils.isEmpty(accountType) || TextUtils.isEmpty(accountName)) {
		    throw new IllegalArgumentException("Sync adapters must specify an account and account type: " + uri);
		}

		// only sync adapters can delete tasks
		count = mTaskDb.delete(Tables.TASKS, selection, selectionArgs);
	    } else {
		// mark task as deleted and dirty, the sync adapter will remove it later
		ContentValues values = new ContentValues();
		values.put(TaskContract.Tasks._DELETED, true);
		values.put(TaskContract.Tasks._DIRTY, true);
		count = mTaskDb.update(Tables.TASKS, values, selection, selectionArgs);
	    }
	    break;
	case CATEGORIES:
	    break;
	case CATEGORY_ID:
	    break;
	default:
	    throw new IllegalArgumentException("Unknown URI " + uri);
	}
	getContext().getContentResolver().notifyChange(uri, null);
	return count;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
	long rowId = 0;
	Uri result_uri = null;

	boolean isSyncAdapter = getIsCallerSyncAdapter(uri);
	String accountName = getAccountName(uri);
	String accountType = getAccountType(uri);

	switch (uriMatcher.match(uri)) {
	case LISTS:
	    if (isSyncAdapter) {
		// only sync adapter may create task lists!

		if (TextUtils.isEmpty(accountType) || TextUtils.isEmpty(accountName)) {
		    throw new IllegalArgumentException("Sync adapters must specify an account and account type: " + uri);
		}

		if (values.containsKey(TaskContract.TaskLists._ID)) {
		    throw new IllegalArgumentException("_ID can not be set manually");
		}

		values.put(TaskContract.ACCOUNT_NAME, accountName);
		values.put(TaskContract.ACCOUNT_TYPE, accountType);
		rowId = mTaskDb.insert(Tables.LISTS, "", values);
		result_uri = TaskContract.TaskLists.CONTENT_URI;
	    } else {
		throw new UnsupportedOperationException("Caller must be a sync adapter to create task lists");
	    }
	    break;

	case TASKS:
	    // ensure ACCOUNT_NAME and ACCOUNT_TYPE are in values
	    if (!values.containsKey(TaskContract.Tasks.ACCOUNT_NAME)) {
		if (TextUtils.isEmpty(accountName)) {
		    throw new IllegalArgumentException("Missing ACCOUNT_NAME");
		}
		values.put(TaskContract.Tasks.ACCOUNT_NAME, accountName);
	    }

	    if (!values.containsKey(TaskContract.Tasks.ACCOUNT_TYPE)) {
		if (TextUtils.isEmpty(accountType)) {
		    throw new IllegalArgumentException("Missing ACCOUNT_TYPE");
		}
		values.put(TaskContract.Tasks.ACCOUNT_TYPE, accountType);
	    }

	    validateTaskValues(values, true, isSyncAdapter);

	    if (!isSyncAdapter) {
		// mark task as dirty from the start
		values.put(TaskContract.Tasks._DIRTY, true);
	    }

	    rowId = mTaskDb.insert(Tables.TASKS, "", values);

	    result_uri = TaskContract.Tasks.CONTENT_URI;
	    break;

	case CATEGORIES:
	    rowId = mTaskDb.insert(Tables.CATEGORIES, "", values);
	    result_uri = TaskContract.Categories.CONTENT_URI;
	    break;
	default:
	    throw new IllegalArgumentException("Unknown URI " + uri);
	}

	if (rowId > 0 && result_uri != null) {
	    result_uri = ContentUris.withAppendedId(result_uri, rowId);
	    getContext().getContentResolver().notifyChange(result_uri, null);
	    return result_uri;
	}
	throw new SQLException("Failed to insert row into " + uri);
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
	int count = 0;

	boolean isSyncAdapter = getIsCallerSyncAdapter(uri);
	String accountName = getAccountName(uri);
	String accountType = getAccountType(uri);

	switch (uriMatcher.match(uri)) {
	case LISTS:
	    break;
	case LIST_ID:
	    break;
	case TASKS:
	    // validate tasks
	    validateTaskValues(values, false, isSyncAdapter);

	    selection = updateSelection(selectAccount(uri), selection);

	    if (!isSyncAdapter) {
		// mark task as dirty
		values.put(TaskContract.Tasks._DIRTY, true);
	    }

	    // perform updates
	    count = mTaskDb.update(Tables.TASKS, values, selection, selectionArgs);
	    break;
	case TASK_ID:
	    selection = updateSelection(selectId(uri), selection);

	    validateTaskValues(values, false, isSyncAdapter);

	    if (!isSyncAdapter) {
		// mark task as dirty
		values.put(TaskContract.Tasks._DIRTY, true);
	    }
	    count = mTaskDb.update(Tables.TASKS, values, selection, selectionArgs);
	    break;
	case CATEGORIES:
	    break;
	case CATEGORY_ID:
	    break;
	default:
	    throw new IllegalArgumentException("Unknown URI " + uri);
	}
	getContext().getContentResolver().notifyChange(uri, null);
	return count;
    }

    /**
     * Validate the given values.
     * 
     * @param values
     *            The task properties to validate.
     * @throws IllegalArgumentException
     *             if any of the values is invalid.
     */
    private void validateTaskValues(ContentValues values, boolean isNew, boolean isSyncAdapter) {
	if (values.containsKey(TaskContract.Tasks._ID)) {
	    throw new IllegalArgumentException("_ID can not be set manually");
	}

	if (isNew != values.containsKey(TaskContract.Tasks.LIST_ID)) {
	    throw new IllegalArgumentException("LIST_ID is write-once and required on INSERT");
	}

	if (isNew != values.containsKey(TaskContract.Tasks.ACCOUNT_NAME)) {
	    throw new IllegalArgumentException("ACCOUNT_NAME is write-once and required on INSERT");
	}

	if (isNew != values.containsKey(TaskContract.Tasks.ACCOUNT_TYPE)) {
	    throw new IllegalArgumentException("ACCOUNT_TYPE is write-once and required on INSERT");
	}

	// no one can undelete a task!
	if (values.containsKey(TaskContract.Tasks._DELETED)) {
	    throw new IllegalArgumentException("modification of _DELETE is not allowed");
	}

	// only sync adapters are allowed to remove the dirty flag
	if (values.containsKey(TaskContract.Tasks._DIRTY) && !isSyncAdapter) {
	    throw new IllegalArgumentException("modification of _DIRTY is not allowed");
	}

	if (values.containsKey(TaskContract.Tasks.PERCENT_COMPLETE)) {
	    Integer percent = values.getAsInteger(TaskContract.Tasks.PERCENT_COMPLETE);
	    if (percent != null && (percent < 0 || percent > 100)) {
		throw new IllegalArgumentException("PERCENT_COMPLETE must be null or an integer between 0 and 100");
	    }
	}

	if (values.containsKey(TaskContract.Tasks.PRIORITY)) {
	    Integer priority = values.getAsInteger(TaskContract.Tasks.PRIORITY);
	    if (priority != null && (priority < 0 || priority > 9)) {
		throw new IllegalArgumentException("PRIORITY must be an integer between 0 and 9");
	    }
	}

	if (values.containsKey(TaskContract.Tasks.CLASSIFICATION)) {
	    Integer classification = values.getAsInteger(TaskContract.Tasks.CLASSIFICATION);
	    if (classification != null && (classification < 0 || classification > 2)) {
		throw new IllegalArgumentException("CLASSIFICATION must be an integer between 0 and 2");
	    }
	}
    }

    @Override
    public String getType(Uri uri) {
	switch (uriMatcher.match(uri)) {
	case LISTS:
	    return "mvnd.android.cursor.dir/vnd.ms.jung.tasklists ";
	case LIST_ID:
	    return "vnd.android.cursor.item/vnd.ms.jung.tasklists ";
	case TASKS:
	    return "mvnd.android.cursor.dir/vnd.ms.jung.tasks ";
	case TASK_ID:
	    return "vnd.android.cursor.item/vnd.ms.jung.tasks ";
	case CATEGORIES:
	    return "mvnd.android.cursor.dir/vnd.ms.jung.categories ";
	case CATEGORY_ID:
	    return "vnd.android.cursor.item/vnd.ms.jung.categories ";
	default:
	    throw new IllegalArgumentException("Unsupported URI: " + uri);
	}
    }

    static {
	uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
	uriMatcher.addURI(TaskContract.AUTHORITY, TaskContract.TaskLists.CONTENT_URI_PATH, LISTS);
	uriMatcher.addURI(TaskContract.AUTHORITY, TaskContract.TaskLists.CONTENT_URI_PATH + "/#", LIST_ID);
	uriMatcher.addURI(TaskContract.AUTHORITY, TaskContract.Tasks.CONTENT_URI_PATH, TASKS);
	uriMatcher.addURI(TaskContract.AUTHORITY, TaskContract.Tasks.CONTENT_URI_PATH + "/#", TASK_ID);
	uriMatcher.addURI(TaskContract.AUTHORITY, TaskContract.Categories.CONTENT_URI_PATH, CATEGORIES);
	uriMatcher.addURI(TaskContract.AUTHORITY, TaskContract.Categories.CONTENT_URI_PATH + "/#", CATEGORY_ID);
    }

}